"""

This module contains the file and cache class used by the client proxy

    Page      - Proxy memory page
    PageList  - linked list of pages belonging to file
    PageCache - LRU cache storing linked list of all memory pages 
    File      - File like object returned to the user

"""



import sys,cmd,string
import os, io, errno
import shutil,time

from dfs_common import *

# decide if python open mode allows writes
def allow_write(mode):
    if 'w' in mode: return True
    if 'a' in mode: return True
    if 'r' in mode and '+' in mode: return True
    return False
    
# decide if python open mode allows reads
def allow_read(mode):
    if 'r' in mode: return True
    if 'a' in mode and '+' in mode: return True
    if 'w' in mode and '+' in mode: return True
    return False

def gen_page_key(fid, pageno):
    return '%s:%d' %( fid, pageno)

def pos2page(pos):
    "Convert file pos to page number, page offset & page pos"
    page_num,offset = divmod(pos, DFS_PAGESIZE)
    page_pos = page_num * DFS_PAGESIZE
    return page_num, offset, page_pos

#
# This represents a memory page in our cache.
# We use multi-link lists to hold a page in both the lru cache
# linked list and in the files own page list
#
#
class Page:

    def __init__(self, fsfid, pageno, data, checked_time):
        # page info
        self.fsfid = fsfid
        self.pageno = pageno
        self.data = data
        self.checked_time = checked_time
        # linked list stuff
        self.prevLRU = None
        self.nextLRU = None
        # used for file
        self.nextPage = None
        self.prevPage = None

    # is interval between last check and given time less than limit
    def check_fresh(self, timestamp):
        return (timestamp - page.checked_time) < DFS_INTERVAL

    def gen_key(self):
        return gen_page_key(self.fsfid, self.pageno)

    def get_data(self):
        return self.data

    def set_data(self,data):
        self.data = data

    def set_checked(self, checked_time):
        self.checked_time = checked_time

    def get_fsfid(self):  
        return self.fsfid

    # lru linked list
    def insertBetweenLRU(self, leftLink, rightLink):
        self.prevLRU = leftLink
        self.nextLRU = rightLink
        leftLink.nextLRU = self
        rightLink.prevLRU = self

    def insertBeforeLRU(self, node):
        self.insertBetweenLRU(node.prevLRU, node)

    def insertAfterLRU(self, node):
        self.insertBetweenLRU(node,node.nextLRU)
        
    def removeLRU(self):
        # point prev node to next
        if (self.prevLRU):
            self.prevLRU.nextLRU = self.nextLRU
        # point next to prev
        if (self.nextLRU):
            self.nextLRU.prevLRU = self.prevLRU
        self.prevLRU = None
        self.nextLRU = None

    def next_lru(self):
        return self.nextLRU

    # file linked list
    def insertBetweenPage(self, leftLink, rightLink):
        self.prevPage = leftLink
        self.nextPage = rightLink
        leftLink.nextPage = self
        rightLink.prevPage = self

    def insertBeforePage(self, node):
        self.insertBetweenPage(node.prevPage, node)

    def insertAfterPage(self, node):
        self.insertBetweenPage(node,node.nextPage)
        
    def removePage(self):
        # point prev node to next
        if (self.prevPage):
            self.prevPage.nextPage = self.nextPage
        # point next to prev
        if (self.nextPage):
            self.nextPage.prevPage = self.prevPage
        self.prevPage = None
        self.nextPage = None

    def next_page(self):
        return self.nextPage


#
# This is container for all the cached pages for a file
# we used a doubly linked list for the pages instead of
# a list because these pages must also exist in the lru
#
class PageList:

    def __init__(self):
        self.fattr = None
        self.ref_count = 0
        # create sentinel
        self.page_list = Page(None, None, None, None)
        self.page_list.nextPage = self.page_list
        self.page_list.prevPage = self.page_list

    def get_fattr(self):
        return self.fattr

    def set_fattr(self, fattr):
        self.fattr = fattr

    def isEmpty(self):
        return self.page_list.next_page() == self.page_list

    def getFirst(self):
        return self.page_list.next_page()

    def getHead(self):
        return self.page_list

    def add_page(self, page):
        page.insertBeforePage(self.page_list)

    def inc_refcnt(self):
        self.ref_count += 1
        return self.ref_count

    def dec_refcnt(self):
        if self.ref_count > 0:
            self.ref_count -= 1
        return self.ref_count

#
# This is a contained for all the cached pages in the proxy
# It implemetns a least recently used (LRU) cache for all file
# pages requiring a linked list. (python dev version does
# not support ordered dictionaries).
#
class PageCache:

    def __init__(self, max_page):
        self.max_page = max_page
        self.num_page = 0
        self.lru_dict = {}
        self.file_dict = {}
        # create sentinel
        self.lru_list = Page(None, None, None, None)
        self.lru_list.nextLRU = self.lru_list
        self.lru_list.prevLRU = self.lru_list

    # remove a page in the cache
    def discard_page(self, page):
        # remove lru list
        page.removeLRU()
        key = page.gen_key()
        del self.lru_dict[key]
        # from from file list
        page.removePage()
        self.num_page -= 1

    # remove first page in the cache
    def discard_lru(self):
        page = self.lru_list.nextLRU()
        if page != self.lru_list:
            # discard 
            self.discard_page(page)
            # discard empty list
            page_list = self.file_dict[page.get_fid()]
            if page_list.isEmpty():
                del self.file_dict[page.get_fid()]

    # get a list of pages for a file
    def get_page_list(self, fsfid):
        page_list = self.file_dict.get(fsfid, None)
        if not page_list:
            page_list = PageList()
            self.file_dict[fsfid] = page_list
        return page_list

    def set_checked(self, fsfid, checked_time):
        page_list = self.get_page_list(fsfid)
        page = page_list.getFirst()
        while page != page_list.getHead():
            page.checked_time = checked_time
            page = page.next_page()

    def get_fattr(self, fsfid):
        page_list = self.get_page_list(fsfid)
        return page_list.get_fattr()

    # update the file attributes
    def set_fattr(self, fsfid, fattr, prev_fattr=None):
        # first get our exiting view of the file
        page_list = self.get_page_list(fsfid)
        our_fattr = page_list.get_fattr()

        if prev_fattr:
            # compare servers before write attr with ours
            cache_valid = prev_fattr.match_mattr(our_fattr)
        elif fattr is None:
            # override - reset cache
            cache_valid = False
        else:
            # compare servers current attr with our own
            cache_valid = fattr.match_mattr(our_fattr)
        
        if fattr:
            page_list.set_fattr(fattr)

        #if our_fattr: print 'ours', our_fattr
        #if fattr: print 'given', fattr
        #if prev_fattr: print 'prev', prev_fattr

        # check if cache attrs different to server
        if not cache_valid:
            #print 'Dumping cache', fsfid
            while not page_list.isEmpty():
                page = page_list.getFirst()
                self.discard_page(page)

        return cache_valid

    # we track open/close of server files here
    def open(self, fsfid):
        page_list = self.get_page_list(fsfid)
        return page_list.inc_refcnt()

    def close(self, fsfid):
        page_list = self.get_page_list(fsfid)
        return page_list.dec_refcnt()

    # get a memory page for a file
    def get_page(self, fsfid, pageno):
        key = gen_page_key(fsfid,pageno)
        try:
            page = self.lru_dict[key]
            # move to end of list
            page.removeLRU()
            page.insertBeforeLRU(self.lru_list)
        except KeyError:
            page = None
        return page

    # set a memory page entry (add/update)
    def set_page(self, page):

        # discard previous entry
        key = page.gen_key()
        if key in self.lru_dict:
            prev = self.lru_dict[key]
            prev.removeLRU()
            prev.removePage()
            self.num_page -= 1

        # discard oldest entry if no room
        if self.num_page >= self.max_page:
            self.discard_lru()

        # add to list of pages per file
        page.removePage()
        page_list = self.get_page_list(page.get_fsfid())
        page_list.add_page(page)

        # add to list of pages per cache
        page.removeLRU()
        self.lru_dict[key] = page
        page.insertBeforeLRU(self.lru_list)

        self.num_page += 1

#
# This represents a python file like object that is handed back
# to user when they ask it to open a file.
# It supports read/writes/flush/seek/close
#
class File:

    def __init__(self, proxy, fserver, fid, tid, mode):
        self.proxy = proxy
        self.fserver = fserver
        self.fid = fid
        self.tid = tid
        self.mode = mode
        self.pos = 0
        # cached writes from client
        self.write_cache = []
        self.write_cache_len = 0
        self.allow_read = allow_read(mode)
        self.allow_write = allow_write(mode)
        # check if appending
        if 'a' in mode:
            self.seek(0, os.SEEK_END)

    # close file object
    def close(self):
        try:
            self.flush()
        finally:
            self.proxy.close_file(self)

    # seek to a file offset
    def seek(self, offset, whence = os.SEEK_SET):
        if whence == os.SEEK_SET:
            self.pos = offset
        elif whence == os.SEEK.CUR:
            self.pos += offset
        elif whence == os.SEEK_END:
            fattr = self.proxy.page_cache.get_fattr(self.fid)
            if fattr is None:
                fattr = self.fserver.stat(fid)
            self.pos = fattr.size + offset
        else:
            rc = errno.EINVAL
            raise IOError(rc, os.strerror(rc), afile.fid)
        return self.pos

    # flush cached writes
    def flush(self):
        if self.write_cache:
            data = "".join(self.write_cache)
            self.write_cache = []
            self.write_cache_len = 0
            self.proxy.write_file(self, self.pos, data)
            self.pos += len(data)

    # read data from proxy
    def read(self, size=-1):
        if not self.allow_read:
            rc = errno.EBADF
            raise IOError(rc, os.strerror(rc), afile.fid)
        buffers = []
        if size < 0:
            # read everything up until eof
            while True:
                num_read = self.proxy.read_file(buffers, self, self.pos, DFS_BUFSIZE)
                self.pos += num_read
                if num_read < DFS_BUFSIZE:
                    # partial block means eof
                    break
        else:
            num_read = self.proxy.read_file(buffers, self, self.pos, size)
            self.pos += num_read
        return "".join(buffers)

    # write data to proxy - cache it if small write
    def write(self, data):
        if not data: return
        if not self.allow_write:
            rc = errno.EBADF
            raise IOError(rc, os.strerror(rc), afile.fid)
        self.write_cache.append(data)
        self.write_cache_len += len(data)
        if self.write_cache_len >= DFS_BUFSIZE:
            self.flush()

